unit Rails.MetaData;

interface

uses
  RTTI, Rails.ActiveRecord.Attributes, Aurelius.Mapping.Attributes, System.TypInfo,
  Aurelius.Mapping.Metadata, Aurelius.Mapping.Explorer, System.Generics.Collections;

type
  TColumnType = (ctString, ctFloat, ctInteger, ctEnumerator, ctDate, ctDateTime, ctTime, ctBoolean, ctMemo, ctBlob);

  TRailsColumn = class
  private
    FDisplayName: string;
    FColumnName: string;
    FLength: Int8;
    FPrecision: Int8;
    FScale: Int8;
    FValidations: TRailsValidations;
    FColumnProperties: TColumnProps;
    FCalculated: Boolean;
    FClass: TClass;
    FVisible: Boolean;
  public
    Min, Max: Variant;
    function IsCalculated: Boolean;
    function ColumnInfo: TColumn;
    function ColumnType: TColumnType;
    constructor Create;
    property DisplayName: string read FDisplayName;
    property ColumnName: string read FColumnName;
    property Validations: TRailsValidations read FValidations;
    property Length: Int8 read FLength;
    property Precision: Int8 read FPrecision;
    property Scale: Int8 read FScale;
    property Visible: Boolean read FVisible;
    property ColumnProperties: TColumnProps read FColumnProperties;
    function Clone: TRailsColumn;
  end;

  TRailsExplorer = class
  private
    class var
      FInstance: TRailsExplorer;
    FColumnsByClass: TObjectDictionary<string, TObjectList<TRailsColumn>>;
    FColumnsByMemberName: TObjectDictionary<string, TRailsColumn>;
    FContext: TRttiContext;
  public
    class function GetIntance: TRailsExplorer;
    function GetInfoByPropName(AClass: TClass; PropName: string): TRailsColumn;
    function GetColumns(AClass: TClass; VisibleOnly: Boolean = False; IncludeParents: Boolean = True): TObjectList<TRailsColumn>;
    function GetColumnByMember(Member: TRttiMember): TRailsColumn;
    function GetText(Instance: TObject; Column: TRailsColumn): string;
    destructor Destroy; override;
    constructor Create;
  end;

implementation

uses
  System.SysUtils, Rails.Utils, DB;


{ TRailsColumn }

function TRailsColumn.Clone: TRailsColumn;
begin
  Result := TRailsColumn.Create;

  Result.FDisplayName := FDisplayName;
  Result.FColumnName := FColumnName;
  Result.FLength := FLength;
  Result.FPrecision := FPrecision;
  Result.FScale := FScale;
  Result.FValidations := FValidations;
  Result.FColumnProperties := FColumnProperties;
  Result.FCalculated := FCalculated;
  Result.FClass := FClass;
  Result.FVisible := FVisible;
end;

function TRailsColumn.ColumnInfo: TColumn;
begin
  Result := TMappingExplorer.DefaultInstance.GetColumnByName(FClass, FColumnName);
end;

function TRailsColumn.ColumnType: TColumnType;
begin
  Result := TColumnType.ctString;
  case ColumnInfo.FieldType of
    ftString,ftWideString: Result := TColumnType.ctString;
    ftExtended,ftFloat,ftCurrency,ftBCD : Result := TColumnType.ctFloat;
    ftDate: Result := TColumnType.ctDate;
    ftTime: Result := TColumnType.ctTime;
    ftDateTime: Result := TColumnType.ctDateTime;
    ftSmallint,ftInteger,ftLargeint,ftShortint:
    begin
      Result := TColumnType.ctInteger;
      if ColumnInfo.Optimization.RealType.TypeKind = tkEnumeration then
        Result := TColumnType.ctEnumerator;
    end;
  end;
end;

constructor TRailsColumn.Create;
begin

end;

function TRailsColumn.IsCalculated: Boolean;
begin
  Result := FCalculated;
end;

constructor TRailsExplorer.Create;
begin
  FColumnsByClass := TObjectDictionary<string, TObjectList<TRailsColumn>>.Create;
  FColumnsByMemberName := TObjectDictionary<string, TRailsColumn>.Create;
  FContext := TRttiContext.Create;
end;

destructor TRailsExplorer.Destroy;
begin
  FreeAndNil(FColumnsByClass);
  FreeAndNil(FColumnsByMemberName);
  inherited;
end;

function TRailsExplorer.GetInfoByPropName(AClass: TClass; PropName: string): TRailsColumn;
var
  Ctx: TRttiContext;
  Typ: TRttiType;
  Prop: TRttiProperty;
begin
  Ctx := TRttiContext.Create;
  Typ := Ctx.GetType(AClass);
  Prop := Typ.GetProperty(PropName);
  Result := GetColumnByMember(Prop);
end;

function TRailsExplorer.GetColumnByMember(Member: TRttiMember): TRailsColumn;
var
  Key: string;
  A: TCustomAttribute;
  RC: RailsColumn;
  C: Column;
  RCC: RailsCalculated;
begin
  Key := Member.Parent.AsInstance.MetaclassType.QualifiedClassName + '.' + Member.Name;
  if FColumnsByMemberName.ContainsKey(Key) then
    Exit(FColumnsByMemberName[Key]);

  Result := TRailsColumn.Create;
  for A in Member.GetAttributes do
  begin
    if A is RailsColumn then
    begin
      RC := RailsColumn(A);
      Result.FDisplayName := RC.DisplayName;
      Result.FValidations := RC.Validations;
      Break;
    end;
    if A is Column then
    begin
      C := Column(A);
      Result.FColumnName := C.Name;
      Result.FLength := C.Length;
      Result.FPrecision := C.Precision;
      Result.FScale := C.Scale;
      Result.FColumnProperties := C.Properties;
      Result.FCalculated := False;
      Break;
    end;
    if A is RailsCalculated then
    begin
      RCC := RailsCalculated(A);
      Result.FDisplayName := RCC.DisplayName;
      Result.FColumnName := RCC.ColumnName;
      Result.FLength := RCC.Length;
      Result.FPrecision := RCC.Precision;
      Result.FScale := RCC.Scale;
      Result.FValidations := RCC.Validations;
      Result.FCalculated := True;
      Break;
    end;
    if A is LessThan then
    begin
      Result.Max := LessThan(A).Value;
      Break;
    end;
    if A is GreaterThan then
    begin
      Result.Min := GreaterThan(A).Value;
      Break;
    end;
    if A is Between then
    begin
      Result.Min := Between(A).Min;
      Result.Max := Between(A).Max;
      Break;
    end;
    if A is Equal then
    begin
      Result.Min := Equal(A).Value;
      Result.Max := Equal(A).Value;
      Break;
    end;
  end;
  FColumnsByMemberName.Add(Key, Result);
end;

function TRailsExplorer.GetColumns(AClass: TClass; VisibleOnly, IncludeParents: Boolean): TObjectList<TRailsColumn>;
var
  Typ: TRttiType;
  Mem: TRttiMember;
  Col: TRailsColumn;
  Key: string;
begin
  Key := AClass.QualifiedClassName+'.'+BoolToStr(VisibleOnly);
  if FColumnsByClass.ContainsKey(Key) then
    Exit(FColumnsByClass[Key]);

  Result := TObjectList<TRailsColumn>.Create;
  FColumnsByClass.Add(Key, Result);
  Typ := FContext.GetType(AClass);

  if IncludeParents and (AClass.ClassParent <> TObject) then
    Result.AddRange(GetColumns(AClass.ClassParent, IncludeParents, False));

  for Mem in Typ.GetDeclaredFields do
  begin
    Col := GetColumnByMember(Mem);
    if (Col <> nil) and ((not VisibleOnly) or (Col.Visible)) then
      Result.Add(Col);
  end;

  for Mem in Typ.GetDeclaredProperties do
  begin
    Col := GetColumnByMember(Mem);
    if (Col <> nil) and ((not VisibleOnly) or (Col.Visible)) then
      Result.Add(Col);
  end;
end;

class function TRailsExplorer.GetIntance: TRailsExplorer;
begin
  if FInstance = nil then
    FInstance := TRailsExplorer.Create;
  Result := FInstance;
end;

function TRailsExplorer.GetText(Instance: TObject; Column: TRailsColumn): string;
var
  V: TValue;
  Attr: TCustomAttribute;
  Index: Int64;
  EnumDisplay: string;
  FormatDate: string;
begin
  Result := '';
  V := TMappingExplorer.DefaultInstance.GetMemberValue(Instance,Column.ColumnInfo.Optimization);

  if TRailsValidate.rvMoney in Column.FValidations then
    Result := FloatToStrF(V.AsExtended, TFloatFormat.ffCurrency, Column.Precision, Column.Scale)
  else if TRailsValidate.rvPercentual in Column.FValidations then
    Result := FloatToStrF(V.AsExtended, TFloatFormat.ffExponent, Column.Precision, Column.Scale)
  else if TRailsValidate.rvZeros in Column.FValidations then
    Result := TRailsUtils.Zeros(V.AsInt64, Column.Length);

  case Column.ColumnType of
    ctDate: FormatDate := FormatSettings.ShortDateFormat;
    ctTime: FormatDate := FormatSettings.ShortTimeFormat;
    ctDateTime: FormatDate := FormatSettings.ShortDateFormat+' '+FormatSettings.ShortTimeFormat;
  end;
  case Column.ColumnType of
    ctString: Result := TMappingExplorer.DefaultInstance.GetMemberValue(Instance, Column.ColumnInfo.Optimization).AsString;
    ctdate, ctTime, ctDateTime:
    begin
      if TMappingExplorer.DefaultInstance.GetMemberValue(Instance, Column.ColumnInfo.Optimization).AsVariant <> 0 then
        Result := FormatDateTime(FormatDate, TMappingExplorer.DefaultInstance.GetMemberValue(Instance, Column.ColumnInfo.Optimization).AsExtended);
      Result := Result + ' ';
    end;
    ctEnumerator:
    begin
      Result := ' ';
      Index := TMappingExplorer.DefaultInstance.GetColumnDbValue(Instance, Column.ColumnInfo);
      for Attr in Column.ColumnInfo.Optimization.MemberType.GetAttributes do
      begin
        if Attr is RailsEnumeration then
        begin
          EnumDisplay := (Attr as RailsEnumeration).Titles;
          Result := TRailsUtils.TokenN(EnumDisplay, Index+2, '|');
        end;
      end;
    end;
    ctBoolean:
    begin
      for Attr in Column.ColumnInfo.Optimization.MemberRef.GetAttributes do
      begin
        if Attr is BooleanValues then
          Result := (Attr as BooleanValues).GetValue(TMappingExplorer.DefaultInstance.GetMemberValue(Instance, Column.ColumnInfo.Optimization).AsBoolean);
      end;
    end;
  end;
  if Result = EmptyStr then
    Result := TMappingExplorer.DefaultInstance.GetMemberValue(Instance, Column.ColumnInfo.Optimization).ToString;
end;

initialization

finalization
  TRailsUtils.TryFreeAndNil(TRailsExplorer.FInstance);
end.
